<?php
namespace App\Libs;
use Exception;
class RequestClient
{
    const APP_NAME = 'xqc';
    const APP_VERSION = '2.0.1';

    private $_accountId = null;

    private $_headerContents;

    private $_responseBodyContents;

    private $_awsAccessKeyId = null;

    private $_awsSecretAccessKey = null;

    private $_userAgent = null;

    private $_config = array ('ProxyHost' => null,
                              'ProxyPort' => -1,
                              'ProxyUsername' => null,
                              'ProxyPassword' => null);

    public function __construct($accountId, $awsAccessKeyId, $awsSecretAccessKey, $config = array())
    {
        $this->_accountId = $accountId;
        if($this->_validateParam( $awsAccessKeyId, $awsSecretAccessKey)){
            $this->_awsAccessKeyId = $awsAccessKeyId;
            $this->_awsSecretAccessKey = $awsSecretAccessKey;
        }else{
            report(new Exception('Invalid Client param ['.$accountId.']',114));
            die(0);
        }
        if ($this->_validateConfig($config)) {
            $this->_config = array_merge($this->_config, $config);
        }else{
            report(new Exception('Invalid Client configuration ['.$accountId.']',114));
            die(0);
        }
        $this->_constructUserAgentHeader();
    }

    private function _validateParam($awsAccessKeyId, $awsSecretAccessKey){
        if(!$awsAccessKeyId)return false;
        if(!$awsSecretAccessKey)return false;
        return true;
    }

    private function _validateConfig($config){
        if(!$config)return false;
        if(!isset($config['ServiceURL'])||empty($config['ServiceURL']))return false;
        if(!isset($config['ServiceVersion'])||empty($config['ServiceVersion']))return false;
        if(!isset($config['MWSClientVersion'])||empty($config['MWSClientVersion']))return false;
        return true;
    }

    /**
     * 设置请求标头User-Agent
     */
    private function _constructUserAgentHeader() {
        $userAgent =
            $this->_quoteApplicationName(self::APP_NAME)
            . '/'
            . $this->_quoteApplicationVersion(self::APP_VERSION);

        $userAgent .= ' (';
        $userAgent .= 'Language=PHP/' . phpversion();
        $userAgent .= '; ';
        $userAgent .= 'Platform=' . php_uname('s') . '/' . php_uname('m') . '/' . php_uname('r');
        $userAgent .= '; ';
        $userAgent .= 'MWSClientVersion=' . $this->_config['MWSClientVersion'];
        $userAgent .= ')';
        $this->_userAgent = $userAgent;
    }

    /**
     * Collapse multiple whitespace characters into a single ' ' character.
     * @param $s
     * @return string
     */
    private function _collapseWhitespace($s) {
        return preg_replace('/ {2,}|\s/', ' ', $s);
    }

    /**
     * Collapse multiple whitespace characters into a single ' ' and backslash escape '\',
     * and '/' characters from a string.
     * @param $s
     * @return string
     */
    private function _quoteApplicationName($s) {
        $quotedString = $this->_collapseWhitespace($s);
        $quotedString = preg_replace('/\\\\/', '\\\\\\\\', $quotedString);
        $quotedString = preg_replace('/\//', '\\/', $quotedString);
        return $quotedString;
    }

    /**
     * Collapse multiple whitespace characters into a single ' ' and backslash escape '\',
     * and '(' characters from a string.
     * @param $s
     * @return string
     */
    private function _quoteApplicationVersion($s) {
        $quotedString = $this->_collapseWhitespace($s);
        $quotedString = preg_replace('/\\\\/', '\\\\\\\\', $quotedString);
        $quotedString = preg_replace('/\\(/', '\\(', $quotedString);
        return $quotedString;
    }

    /**
     * execute request
     * @param array $param
     * @param null  $fileName
     * @return mixed|null
     */
    public function requestExe(array $param, $fileName = null)
    {
        $query = $this->_addRequiredParameters($param);
        $parameters = $this->_getParametersAsString($query);
        return is_null($fileName)?$this->_post($parameters):$this->_reportPost($parameters,$fileName);
    }

    // Private API ------------------------------------------------------------//

    /**
     * Add authentication related and version parameters
     * @param array $parameters
     * @return array
     */
    private function _addRequiredParameters(array $parameters)
    {
        $parameters['AWSAccessKeyId'] = $this->_awsAccessKeyId;
        $parameters['Timestamp'] = $this->_getFormattedTimestamp();
        $parameters['Version'] = $this->_config['ServiceVersion'];
        $parameters['SignatureVersion'] = 2;
        $parameters['SignatureMethod'] = 'HmacSHA256';
        $parameters['Signature'] = $this->_signParameters($parameters, $this->_awsSecretAccessKey);
        return $parameters;
    }

    private function _getFormattedTimestamp()
    {
        return gmdate("Y-m-d\TH:i:s.\\0\\0\\0\\Z", time());
    }

    private function _signParameters(array $parameters, $key) {
        $hash = 'sha256';
        $stringToSign = $this->_calculateStringToSignV2($parameters);
        return base64_encode(hash_hmac($hash, $stringToSign, $key, true));
    }

    private function _calculateStringToSignV2(array $parameters) {
        $parsedUrl = parse_url($this->_config['ServiceURL']);
        $data = 'POST';
        $data .= "\n";
        $data .= $parsedUrl['host'];
        $data .= "\n";
        $uri = array_key_exists('path', $parsedUrl) ? $parsedUrl['path'] : "/";
        $uriEncoded = implode("/", array_map(array($this, "_urlEncode"), explode("/", $uri)));
        $data .= $uriEncoded;
        $data .= "\n";
        uksort($parameters, 'strcmp');
        $data .= $this->_getParametersAsString($parameters);
        return $data;
    }

    /**
     * Convert parameters to Url encoded query string
     * @param array $parameters
     * @return string
     */
    private function _getParametersAsString(array $parameters)
    {
        $queryParameters = array();
        foreach ($parameters as $key => $value) {
            $queryParameters[] = $key . '=' . $this->_urlEncode($value);
        }
        return implode('&', $queryParameters);
    }

    private function _urlEncode($value) {
        return str_replace('%7E', '~', rawurlencode($value));
    }

    private function _getDefaultCurlOptions() {
        $url = parse_url ($this->_config['ServiceURL']);
        $uri = array_key_exists('path', $url) ? $url['path'] : null;
        if (!isset ($uri)) {
            $uri = "/";
        }
        switch ($url['scheme']) {
            case 'https':
                $scheme = 'https://';
                $port = isset($url['port']) ? $url['port'] : 443;
                break;
            default:
                $scheme = 'http://';
                $port = isset($url['port']) ? $url['port'] : 80;
        }
        $options = [
            CURLOPT_URL => $scheme . $url['host'] . $uri,
            CURLOPT_PORT => $port,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
//            CURLOPT_CAINFO => '/etc/ssl/cacert.pem',

//            CURLOPT_SSL_VERIFYPEER => false,
//            CURLOPT_SSL_VERIFYHOST => false,
//            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
//            CURLOPT_SSLCERTTYPE => 'PEM',
//            CURLOPT_SSLCERT => '/etc/ssl/4690444__xinqianchao.com.pem',
//            CURLOPT_SSLKEYTYPE => 'PEM',
//            CURLOPT_SSLKEY => '/etc/ssl/4690444__xinqianchao.com.key',

            CURLOPT_USERAGENT => $this->_userAgent,
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 60,
            CURLOPT_TIMEOUT => 120,
        ];
        if ($this->_config['ProxyHost'] != null && $this->_config['ProxyPort'] != -1)
        {
            $options[CURLOPT_PROXY] = $this->_config['ProxyHost'] . ':' . $this->_config['ProxyPort'];
        }
        if ($this->_config['ProxyUsername'] != null && $this->_config['ProxyPassword'] != null)
        {
            $options[CURLOPT_PROXYUSERPWD] = $this->_config['ProxyUsername'] . ':' . $this->_config['ProxyPassword'];
        }
        return $options;
    }

    private function _post($parameters)
    {
        $options = $this->_getDefaultCurlOptions();
        $options[CURLOPT_POSTFIELDS] = $parameters;
        $options[CURLOPT_HTTPHEADER] = ["Content-Type: application/x-www-form-urlencoded; charset=utf-8","Expect: "];
        $options[CURLOPT_HEADER] = true;
        $curlClient = curl_init();
        curl_setopt_array($curlClient, $options);
        $response = curl_exec($curlClient);
        $code = (int) curl_getinfo($curlClient, CURLINFO_HTTP_CODE);
        if ($response === false || $code != 200) {
            $msg = $response === false ? curl_error($curlClient) : "Failed to parse body : status(" . $code . ")response(" . $response . ")";
            $errno = $response === false ? curl_errno($curlClient) : $code;
            report(new Exception('['.$this->_accountId.']'.$msg,$errno));
            curl_close($curlClient);
            return false;
        }
        curl_close($curlClient);
        return $this->_extractHeadersAndBody($response);
    }

    private function _extractHeadersAndBody($response){
        //First split by 2 'CRLF'
        $responseComponents = preg_split("/(?:\r?\n){2}/", $response, 2);
        $code = null;$body = null;
        for ($count = 0; $count < count($responseComponents) && $body == null; $count++) {
            $headers = $responseComponents[$count];
            $code = $this->_extractHttpStatusCode($headers);
            if($code != null && $this->_httpHeadersHaveContent($headers)){
                //The body will be the next item in the responseComponents array
                $body = $responseComponents[++$count];
            }
        }
        if($body == null || $code != 200){
            report(new Exception('['.$this->_accountId.']'."Failed to parse body : status(" . $code . ")response(" . $response . ")",115));
            return false;
        }
        return xmlToArr($body);
    }

    /**
     * parse the status line of a header string for the proper format and
     * return the status code
     *
     * Example: HTTP/1.1 200 OK
     * ...
     * returns String statusCode or null if the status line can't be parsed
     * @param $headers
     * @return null
     */
    private function _extractHttpStatusCode($headers){
        $statusCode = null;
        if (1 === preg_match("/(\\S+) +(\\d+) +([^\n\r]+)(?:\r?\n|\r)/", $headers, $matches)) {
            //The matches array [entireMatchString, protocol, statusCode, the rest]
            $statusCode = $matches[2];
        }
        return $statusCode;
    }

    /**
     * Tries to determine some valid headers indicating this response
     * has content.  In this case
     * return true if there is a valid "Content-Length" or "Transfer-Encoding" header
     * @param $headers
     * @return bool
     */
    private function _httpHeadersHaveContent($headers){
        return (1 === preg_match("/[cC]ontent-[lL]ength: +(?:\\d+)(?:\\r?\\n|\\r|$)/", $headers) ||
            1 === preg_match("/Transfer-Encoding: +(?!identity[\r\n;= ])(?:[^\r\n]+)(?:\r?\n|\r|$)/i", $headers));
    }

    private function _reportPost($parameters,$fileName)
    {
        $options = $this->_getDefaultCurlOptions();
        $options[CURLOPT_VERBOSE] = true;
        $options[CURLOPT_HEADERFUNCTION] = array($this, 'headerCallback');
        $options[CURLOPT_POSTFIELDS] = $parameters;
        $this->_responseBodyContents = @fopen($this->_getReportFilePath($fileName), 'w+');
        $options[CURLOPT_WRITEFUNCTION] = array($this, 'responseCallback');

        $curlClient = curl_init();
        curl_setopt_array($curlClient, $options);
        $this->_headerContents = @fopen('php://memory', 'rw+');

        $response = curl_exec($curlClient);

        //header
        rewind($this->_headerContents);
        $header = stream_get_contents($this->_headerContents);
        $parsedHeader = $this->_parseHttpHeader($header);

        $code = (int) curl_getinfo($curlClient, CURLINFO_HTTP_CODE);

        $flag = false;
        $msg = null;
        //verify Content-MD5
        if ($response === false || $code != 200) {
            $msg = $response === false ? curl_error($curlClient):"Failed to parse body : status(" . $code . ")response(" . $response . ")";
            $flag = true;
        }else{
            if(!$this->_verifyContentMd5($this->_getParsedHeader($parsedHeader,'Content-MD5'),$this->_responseBodyContents)){
                $msg = 'ContentMD5 Does Not Match.';
                $flag = true;
            }
        }
        @fclose($this->_responseBodyContents);
        @fclose($this->_headerContents);
        curl_close($curlClient);
        if($flag){
            report(new Exception('['.$this->_accountId.']'.$msg,115));
            return false;
        }
        return $response;
    }

    private function headerCallback($ch, $string) {
        return fwrite($this->_headerContents, $string);
    }

    private function responseCallback($ch, $string) {
        return fwrite($this->_responseBodyContents, $string);
    }

    /**
     * Md5 verify data transfer complete
     * @param $receivedMd5Hash
     * @param $streamHandle
     * @return bool
     */
    private function _verifyContentMd5($receivedMd5Hash, $streamHandle) {
        rewind($streamHandle);
        $expectedMd5Hash = $this->_getContentMd5($streamHandle);
        rewind($streamHandle);
        if (!($receivedMd5Hash === $expectedMd5Hash)) {
            return false;
        }
        return true;
    }

    /**
     * get Md5
     * @param $data
     * @return string
     */
    private function _getContentMd5($data) {
        $md5Hash = null;
        if (is_string($data)) {
            $md5Hash = md5($data, true);
        } else if (is_resource($data)) {
            $streamMetadata = stream_get_meta_data($data);
            if ($streamMetadata['stream_type'] === 'MEMORY' || $streamMetadata['stream_type'] === 'TEMP') {
                $md5Hash = md5(stream_get_contents($data), true);
            } else {
                $md5Hash = md5_file($streamMetadata['uri'], true);
            }
        }
        return base64_encode($md5Hash);
    }

    /**
     * Parse HTTP header information
     * @param $header
     * @return array
     */
    private function _parseHttpHeader($header) {
        $parsedHeader = array ();
        foreach (explode("\n", $header) as $line) {
            $splitLine = preg_split('/:\s/', $line, 2, PREG_SPLIT_NO_EMPTY);

            if (sizeof($splitLine) == 2) {
                $k = strtolower(trim($splitLine[0]));
                $v = trim($splitLine[1]);
                if (array_key_exists($k, $parsedHeader)) {
                    $parsedHeader[$k] = $parsedHeader[$k] . "," . $v;
                } else {
                    $parsedHeader[$k] = $v;
                }
            }
        }
        return $parsedHeader;
    }

    /**
     * get heard by key eg:Content-MD5
     * @param $parsedHeader
     * @param $key
     * @return mixed
     */
    private function _getParsedHeader($parsedHeader, $key) {
        return array_key_exists(strtolower($key),$parsedHeader)?$parsedHeader[strtolower($key)]:null;
    }

    /**
     * file path
     * @param $path
     * @return string
     */
    private function _getReportFilePath($path){
        $filePath = storage_path('app/amazon').DIRECTORY_SEPARATOR.$path;
        $fileDir = dirname($filePath);
        if(!is_dir($fileDir)){
            mkdir($fileDir,0777,true);
        }
        return $filePath;
    }
}
